﻿using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;

namespace SharePointUtilitiesForDevelopers
{
    /// <summary>
    /// Contains methods for manipulating SharePoint XML.
    /// </summary>
    public static class SharePointXml
    {
        /// <summary>
        /// Parses the XML.
        /// </summary>
        /// <param name="xml">The XML.</param>
        /// <param name="defaultElementName">The element to create if the xml input is null.</param>
        /// <returns>An <see cref="XmlElement"/>.</returns>
        public static XmlElement ParseXml(string xml, string defaultElementName)
        {
            // If the xml string is null, create an empty node using the element name
            if (string.IsNullOrEmpty(xml))
            {
                if (!string.IsNullOrEmpty(defaultElementName))
                {
                    xml = string.Format("<{0} />", defaultElementName);
                }
            }

            // Return the xml
            return xml != null ? XElement.Parse(xml).ToXmlElement() : null;
        }


        /// <summary>
        /// Gets the attribute.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The attribute, or null if not found.</returns>
        public static XAttribute GetAttribute(XElement element, string attributeName)
        {
            if (element == null)
            {
                return null;
            }
            return element.Attributes().Where(
                attr =>
                string.Compare(attr.Name.LocalName, attributeName, StringComparison.InvariantCultureIgnoreCase) == 0).
                FirstOrDefault();

        }

        /// <summary>
        /// Gets the child element.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="childElementName">The child element name.</param>
        /// <returns>The element or null if not found.</returns>
        public static XElement GetChildElement(XElement element, string childElementName)
        {
            return element != null
                       ?
                           element.Descendants().Where(
                               e =>
                               string.Compare(e.Name.LocalName, childElementName,
                                              StringComparison.InvariantCultureIgnoreCase) == 0).FirstOrDefault()
                       :
                           null;
        }

        /// <summary>
        /// Gets the element value as string.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>The element value if the element is not null, empty string otherwise.</returns>
        public static string GetElementValueAsString(XElement element)
        {
            return element != null ? element.Value : string.Empty;
        }

        /// <summary>
        /// Gets the element value as a nullable integer.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>The element value if the element is not null, null otherwise.</returns>
        public static int? GetElementValueAsNullableInteger(XElement element)
        {
            var result = GetElementValueAsString(element);
            return !string.IsNullOrEmpty(result) ? int.Parse(result) : default(int?);
        }

        /// <summary>
        /// Gets the element value as boolean.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>The element value if the element is not null, false otherwise.</returns>
        public static bool GetElementValueAsBoolean(XElement element)
        {
            var result = GetElementValueAsString(element);
            return !string.IsNullOrEmpty(result) ? bool.Parse(result) : false;
        }

        /// <summary>
        /// Gets the child element value as string.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="childElementName">The child element name.</param>
        /// <returns>The element value if the element is not null, null otherwise.</returns>
        public static string GetChildElementValueAsString(XElement element, string childElementName)
        {
            return GetElementValueAsString(GetChildElement(element, childElementName));
        }

        /// <summary>
        /// Gets the child element value as a nullable integer.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="childElementName">The child element name.</param>
        /// <returns>The element value if the element is not null, null otherwise.</returns>
        public static int? GetChildElementValueAsNullableInteger(XElement element, string childElementName)
        {
            return GetElementValueAsNullableInteger(GetChildElement(element, childElementName));
        }

        /// <summary>
        /// Gets the child element value as boolean.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="childElementName">The child element name.</param>
        /// <returns>The element value if the element is not null, null otherwise.</returns>
        public static bool GetChildElementValueAsBoolean(XElement element, string childElementName)
        {
            return GetElementValueAsBoolean(GetChildElement(element, childElementName));
        }

        /// <summary>
        /// Gets the child element attribute value as boolean.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="childElementName">Name of the element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The element value if the element is not null, null otherwise.</returns>
        public static bool GetChildElementAttributeValueAsBoolean(XElement element, string childElementName, string attributeName)
        {
            return GetAttributeAsBoolean(GetChildElement(element, childElementName), attributeName);
        }

        /// <summary>
        /// Gets the child element attribute value as string.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="childElementName">Name of the element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The element value if the element is not null, empty string otherwise.</returns>
        public static string GetChildElementAttributeValueAsString(XElement element, string childElementName, string attributeName)
        {
            return GetAttributeAsString(GetChildElement(element, childElementName), attributeName);
        }

        /// <summary>
        /// Gets the child element attribute value as a nullable integer.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="childElementName">Name of the element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The element value if the element is not null, null otherwise.</returns>
        public static int? GetChildElementAttributeValueAsNullableInteger(XElement element, string childElementName, string attributeName)
        {
            return GetAttributeAsNullableInteger(GetChildElement(element, childElementName), attributeName);
        }
        
        /// <summary>
        /// Gets the attribute value as a Guid.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The attribute value or empty guid if the attribute isn't present.</returns>
        public static Guid GetAttributeAsGuid(XElement element, string attributeName)
        {
            var result = GetAttributeAsString(element, attributeName);
            return (!string.IsNullOrEmpty(result)) ? new Guid(result) : Guid.Empty;
        }

        /// <summary>
        /// Gets the attribute value as a string.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The attribute value or empty string if the attribute isn't found.</returns>
        public static string GetAttributeAsString(XElement element, string attributeName)
        {
            if (element != null)
            {
                var attribute = GetAttribute(element, attributeName);
                if (attribute != null)
                {
                    return attribute.Value;
                }
            }

            return string.Empty;
        }

        /// <summary>
        /// Gets the attribute as enum.
        /// </summary>
        /// <typeparam name="TEnum">The enum type</typeparam>
        /// <param name="element">The element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <param name="ignoreErrors">When true, casting errors are ignored and the default value is returned;</param>
        /// <returns>The attribute value as the specified enum type of the default value.</returns>
        public static TEnum GetAttributeAsEnum<TEnum>(XElement element, string attributeName, TEnum defaultValue, bool ignoreErrors)
        {
            var result = defaultValue;
            var stringValue = GetAttributeAsString(element, attributeName);

            if (!string.IsNullOrEmpty(stringValue))
            {
                try
                {
                    int intValue;
                    var isInteger = int.TryParse(stringValue, out intValue);

                    // Is the value an integer?
                    if (isInteger)
                    {
                        result = (TEnum)Enum.ToObject(typeof(TEnum), intValue);
                    }
                    else // parse the string
                    {
                        result = (TEnum)Enum.Parse(typeof(TEnum), stringValue, true);
                    }
                }
                catch
                {
                    if (!ignoreErrors) throw;
                }
            }

            return result;
        }

        /// <summary>
        /// Gets the attribute as boolean.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <param name="defaultValue">The value if the attribute isn't found or is empty.</param>
        /// <returns>The attribute value or false if the element or the attribute is null.</returns>
        public static bool GetAttributeAsBoolean(XElement element, string attributeName, bool defaultValue)
        {
            var result = GetAttributeAsString(element, attributeName);
            return !string.IsNullOrEmpty(result) ? bool.Parse(result) : defaultValue;
        }
        
        /// <summary>
        /// Gets the attribute as boolean.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The attribute value or false if the element or the attribute is null.</returns>
        public static bool GetAttributeAsBoolean(XElement element, string attributeName)
        {
            return GetAttributeAsBoolean(element, attributeName, false);
        }

        /// <summary>
        /// Gets the attribute as a nullableinteger.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <returns>The attribute value as an integer or null if not present.</returns>
        public static int? GetAttributeAsNullableInteger(XElement element, string attributeName)
        {
            var result = GetAttributeAsString(element, attributeName);
            return !string.IsNullOrEmpty(result) ? int.Parse(result) : default(int?);
        }

        /// <summary>
        /// Splits the value.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="separator">The separator.</param>
        /// <param name="expectedLength">When specified, throws an exception if the resulting array is not of the specified length.</param>
        /// <returns>An array of values.</returns>
        public static string[] SplitValue(string value, string separator, int? expectedLength)
        {
            if (string.IsNullOrEmpty(value))
            {
                return null;
            }

            if (string.IsNullOrEmpty(separator))
            {
                throw new ArgumentException(Properties.Resources.SplitValueSeparatorMissingException);
            }

            var components = Regex.Split(value, separator);

            if (expectedLength.HasValue && (components.Length != expectedLength))
            {
                throw new FormatException(string.Format(Properties.Resources.SplitValueArrayLengthException, components.Length, expectedLength));
            }

            return components;
        }
        
        /// <summary>
        /// Parses the unique id from a list item unique id column.
        /// Example format: ows_UniqueId="1;#{38593BF5-3029-4F9F-A88A-F00A751792F8}".
        /// </summary>
        /// <param name="uniqueIdString">The unique id string.</param>
        /// <returns>A Guid.</returns>
        public static Guid ParseUniqueId(string uniqueIdString)
        {
            // Split the string into its components
            var components = SplitValue(uniqueIdString, ";#", 2);

            return components != null ? new Guid(components[1]) : Guid.Empty;

        }

        /// <summary>
        /// Parses the id from a list item lookup column.
        /// Example format: ows_Country="1;#USA"
        /// </summary>
        /// <param name="lookupString">The lookup string.</param>
        /// <returns>The id of the item in the lookup list.</returns>
        public static int? ParseLookupId(string lookupString)
        {
            // Split the string into its components
            var components = SplitValue(lookupString, ";#", 2);

            return components != null ? int.Parse(components[0]) : default(int?);
        }

        /// <summary>
        /// Parses the URL from a list item url column.
        /// Example format: ows_Image_x0020_Url="http://contoso.com/PublishingImages/newsarticleimage.jpg, SharePoint Stock Photo"
        /// </summary>
        /// <param name="urlString">The URL string.</param>
        /// <returns>A url.</returns>
        public static string ParseUrl(string urlString)
        {
            // Split the string into its components
            var components = SplitValue(urlString, ", ", 2);

            return components != null ? components[0] : null;
        }

        /// <summary>
        /// Parses the URL description from a list item url column.
        /// Example format: ows_Image_x0020_Url="http://contoso.com/PublishingImages/newsarticleimage.jpg, SharePoint Stock Photo"
        /// </summary>
        /// <param name="urlString">The URL string.</param>
        /// <returns>The description of the url.</returns>
        public static string ParseUrlDescription(string urlString)
        {
            // Split the string into its components
            var components = SplitValue(urlString, ", ", 2);

            return components != null ? components[1] : null;
        }

        /// <summary>
        /// Returns this <see cref="XmlElement"/> as an <see cref="XElement"/>.
        /// </summary>
        /// <param name="xmlElement">The XML.</param>
        /// <returns>An <see cref="XElement"/>.</returns>
        public static XElement ToXElement(this XmlElement xmlElement)
        {
            var xmlDoc = new XmlDocument();
            xmlDoc.AppendChild(xmlDoc.ImportNode(xmlElement, true));
            return XElement.Parse(xmlDoc.InnerXml);
        }

        /// <summary>
        /// Returns this <see cref="XElement"/> as an <see cref="XmlElement"/>.
        /// </summary>
        /// <param name="xElement">The XElement.</param>
        /// <returns>An <see cref="XmlElement"/>.</returns>
        public static XmlElement ToXmlElement(this XElement xElement)
        {
            using (var xmlReader = xElement.CreateReader())
            {
                var xmlDoc = new XmlDocument();
                xmlDoc.Load(xmlReader);
                return xmlDoc.DocumentElement;
            }
        }
    }
}
